#!/usr/bin/python -u

# QUNDASH: a QUick'N'Dirty ActionScript to HaXe converter
# by Christophe Dehlinger
# distributed under GPL2.0

import os, os.path, re, sys, sets
from sets import Set

def makeParensRe(base, n):
	""" makeParensBe(base, n) build an uncompiled regexp for expressions
		made of base and nested parens of depth up to n.
		Ex: makeParensRe("[ab]", 2) matches "aa(b)()((abab)ab(b)bb)b"
	"""
	par = base + "*"
	for _ in range(n):
		par = "(" + base + r"|\(" + par + "\))*"
	return par

def iff(test, ifyes, ifno):
	""" functional if to workaround the lack of ?:-like construct in python < 2.5
		Note that iff is eager in all its arguments, so both "ifyes" and "ifno" are
		evaluated regardless of the value of "test".
	"""
	if test: return ifyes
	else: return ifno

def sp(s):
	""" I don't remember what sp stands for (probably "strip parens" or something
		to that effect). sp(str) removes all parens enclosing the whole str string.
		Ex: sp("((( la la) (la la la)) )") --> ( la la) (la la la)
	"""
	s = s.strip()
	if s[0] == "(" and s[-1] == ")":
		depth = 0
		for i in range(1, len(s) - 1):
			if s[i] == "(": depth += 1
			elif s[i] == ")":
				depth -= 1
				if depth < 0: return s
		return sp(s[1:-1])
	else: return s

class MyRE:
	""" A wrapper for the usual python regular expressions that makes them more pleasant
		to use IMHO. They have a similar API as regular regexps, except that they produce
		"match" objects in which matched groups are fields instead of elements of a
		dictionary.
	"""	
	def __init__(self, exp):
		self.rawexp = exp
		self.regexp = re.compile(exp)
		self.fields = [match.group('name') for match in MyRE.re_MyRE.finditer(exp)]
	
	re_MyRE = re.compile(r"\(\?P<(?P<name>[\w]+)>")
    
	class RegExpResult:
		def __init__(self, start, end, str):
			self._st, self._end, self._str = start, end, str
	
	# private
	def buildresult(self, m):
		res = MyRE.RegExpResult(m.start(), m.end(), m.group())
		for f in self.fields:
			setattr(res, f, m.group(f))
		return res
	
	def search(self, str):
		m = self.regexp.search(str)
		if m: return self.buildresult(m)
		else: return None
	
	def finditer(self, str):
		for match in self.regexp.finditer(str):
			yield self.buildresult(match)
	
	def subn(self, st, fnstr):
		#print "startsubn", self.rawexp, st
		if type(fnstr) is str: repl = fnstr
		else: repl = lambda match: fnstr(self.buildresult(match))
		res = self.regexp.subn(repl, st)
		#print "endsubn", res
		return res
	
	def sub(self, str, fn):
		return self.subn(str, fn)[0]
	
	def recsub(self, str, fn):
		#print "start ", self.rawexp, str
		cont = 666
		while cont > 0:
			str, cont = self.subn(str, fn)
			#print "yo", cont, str
		#print "end"
		return str

##################################### ALL REGULAR EXPRESSIONS
#
# Don't panic.
#
# Don't vomit.
#
par = makeParensRe("[^()]", 4)
re_class = MyRE(r"^(?P<kind>\s*(class|interface))\s+((?P<prefix>[\w.]+)\.)?(?P<name>[\w.]+)\s*(?P<ext>extends\s+[\w.]+\s*)?(implements\s+(?P<impl>[\w.]+(\s*,\s*[\w.]+)*)\s*)?(?P<rest>.*)") 
re_commentLine = MyRE(r"^\s*((/?\*)|//)")
re_import = MyRE(r"^\s*import\s*(\w+\.)*(?P<name>[\w.]*)")
regexp_if = r"(((?P<ret>return)?[\w.+/*-]|==)+\s*)?(\(" + par + r"\))?"
re_interro = MyRE(r"(?=.*\?.*:)(?P<ret>\s*(return\s*|))"
			+ r"(?P<test>\(" + par + r"\)|[\w.]+(\(" + par + r"\))?(\s*(<|>|[<>=!]=)([\w.+/*\s-]*)(\(" + par + r"\))?)?)"
			+ r"\s*\?\s*(?P<ifyes>[^?:]*(\?[^?:]*:[^?]*)?):\s*"
			+ r"(?P<ifno>([^?:]|" + par + r")*)")
r = MyRE(r"(?=.*\?.*:)(?P<ret>\s*(return\s*|))"
	+ r"(?P<test>\(" + par + r"\))"
	) #+ r"\s*\?\s*(?P<ifyes>[^?:]*(\?[^?:]*:[^?]*)?):\s*")
regexp_dict = r"(?P<name>[\w.]+)\s*\[\s*(?P<selector>(?P<index>i|j|k|x|y|index|count|\d+[^\]]*|[^\]]*\b\d+)|(?P<str>" + "[\"'][^\"']*[\"'])|(?P<field>[^\\s\"'\\d+-][^[\\]+-]*))\\s*\\]"
re_dict_aff = MyRE(regexp_dict + r"\s*=(?P<value>[^=][^;]*);")
re_dict_exists = MyRE(regexp_dict + r"\s*==\s*(null|undefined)")
re_dict_access = MyRE(regexp_dict)
re_dict_delete = MyRE(r"delete\s+" + regexp_dict)
re_cast = MyRE(r"(?P<isnew>new\s+)?\b(?P<classname>(\w+\.)*[A-Z][\w]*)\s*\((?P<value>" + par + r")\)")
re_for_incr = MyRE(r"for\s*\(\s*var\s+(?P<cnt>[\w.]+)\s*(:\s*\w+\s*)?=\s*(?P<start>[^;]+)\s*;\s*[\w.]+\s*(?P<test><|<=)\s*(?P<max>[^;]+);\s*[\w.]+\s*\+\+\s*\)")

re_for_in = MyRE(r"for\s*\(\s*(var\s+)?(?P<id>\w+)(\s*:\s*String)?\s+in\s*(?P<obj>[^)]*)\)")
re_for_other = MyRE(r"for\s*\(\s*(?P<init>[^;]*);\s*(?P<test>[^;]*);\s*(?P<next>.*)\)(?P<rest>.*{)")
re_fun_decl = MyRE(r"^(?P<pre>\s*((public|private|static)\s+)*)function\s*((?P<setter>set|get)\s+)?(?P<name>[\w.]+)(?P<rest>(\s*(?P<args>\((.|\n)*\))\s*(?P<type>:\s*[\w.]+)?\s*(?P<after>[{;].*)))?")
re_funarg = MyRE(r"(?P<name>\w+)\s*(?P<type>:\s*[\w.]+)?")
re_switch = MyRE(r"\bswitch\b")
re_break = MyRE(r"\bbreak\s*;")
re_instanceof = MyRE(r"(?P<value>[\w.+-[\]]*(\s*\(" + par + r"\))?)\s+instanceof\s+(?P<cl>[\w.]+)")
re_apply = MyRE(r"(?P<fun>[\w.[\]]+(\s*\(" + par + r"\)\s*)?)\.apply\s*\((?P<obj>[^,]*)")
re_startingws = MyRE(r"^\s*")
re_typeof_cmp = MyRE(r"typeof\s*\((?P<value>" + par + r")\)\s*==\s*" + "\"" + r"(?P<cl>[\w.]+)" + "\"")
re_initializer = MyRE(r"(?P<tags>((public|private|static)\s+)*)var\s+(?P<name>\w+)\s*(?P<type>:\s*[\w.]+)?\s*(?P<initvar>=.*)?;")
#I used to have "par" in there but it hangs on "var removed:Array = rowCells.splice(removeIndex, rowCells.length-1);"
re_splice_insert = MyRE(r"\.splice\s*\((?P<index>[^,()]*(" + par + r")?),\s*0+\s*,\s*(?P<value>([^,()]|" + par + r")*)\)")
re_super = MyRE(r"\bsuper\s*\(")
#re_staticvar = MyRE(r"(?P<pub>public\s+)?static\s+var\s+[A-Z_]+")




def buildRewriters():
	""" Rewriters are used to make the simple substitutions (ie one word for another).
		They are applied in order, so order does matter.
	"""
	flashlibs = ["Accessibility", "Camera", "Color", "Key", "LoadVars",
		 		 "LocalConnection", "Microphone", "Mouse", "MovieClip", 
		 		 "MovieClipLoader", "PrintJob", "Selection", "SharedObject",
		 		 "Sound", "Stage", "System", "TextField", "TextFormat", "TextSnapshot",
		 		 "Video", "system.Capabilities", "system.Security"]
	
	# "subs" contains the substitutions that apply only to whole words
	# ie "x:Object" will be changed to "x:Dynamic", but "x:MyObject" will remain unchanged
	# note that Number is changed to Int, you might want to change that
	subs = [ ("_level0", "flash.Lib._root"), ("_root", "flash.Lib.current"),
			 ("_global", "flash.Lib._global"), ("escape", "StringTools.urlEncode"),
			 ("unescape", "StringTools.urlDecode"), 
			 ("getVersion()", "system.capabilities.version"),
			 ("Number.MAX_VALUE", "666666666"), ("Number.MIN_VALUE", "-666666666"),
			 ("Number.NaN", "Std.nan"), ("Number.NEGATIVE_INFINITY", "Math.NEGATIVE_INFINITY"),
			 ("Number.POSITIVE_INFINITY", "Math.POSITIVE_INFINITY"),
			 ("Object.registerClass", "flash.Lib.registerClass"),
			 ("new Object()", "Reflect.empty()"), ("new Error", ""),
			 ("Boolean", "Bool"), ("Object", "Dynamic"), ("undefined", "null"),
			 ("new XML", "Xml.parse"), ("childNodes[0]", "firstChild()"),
			 ("eval", "flash.Lib.eval"), ("getTimer", "flash.Lib.getTimer"),
			 ("Math.max", "Qundash.max"), ("Math.min", "Qundash.min"),
			 ("XMLNode", "Xml"), ("XML", "Xml"), ("slice", "substr"),
			 ("Number", "Int"), ("Array", "Array<Dynamic>"),
			 ("Function", "Void->Dynamic"),
			 ("setInterval", "flash.Lib._global.setInterval"),
			 ("clearInterval", "flash.Lib._global.clearInterval"),
			 ("HashMap", "Hash<Dynamic>"), ("containsKey", "exists")]
	#		 + [("creater", "Component.creater")]   #ad hoc changes
	subs += [(lib, "flash." + lib) for lib in flashlibs]
	
	# "exact" contains the substitutions that do not care about word boundaries
	exact = [(".concat()", ".copy()"), (".put(", ".set("), (".substring(", ".substr(")]
	
	re_esc = MyRE(r"[[.()?+*|]|\]")
	rews = [(MyRE(r"(?P<bef>^|[^\w.])" + re_esc.sub(k, lambda m: "\\" + m._str)
			+ r"(?P<aft>$|[^\w])"), v) for k, v in subs] + [(MyRE(r"(?P<bef>)" + re_esc.sub(k, lambda m: "\\" + m._str) + r"(?P<aft>)"), v) for k, v in exact]
	#for k, v in subs: print r"(?P<bef>^|[^\w.])" + re_esc.sub(k, lambda m: "\\" + m._str) + r"(?P<aft>$|[^\w.]) =>", v
	return rews

rewriters = buildRewriters()


def getLI(filename):
	""" open a file and return an iterator on its lines """
	return iter(open(filename).readlines())

# these vars are global because I couldn't figure out the python way to scope them
# properly. They should ideally be in parseFile.
comments = ""		# extra comments generated by quandash for the current line
fors = []			# there is 
					
depth = 0			# current depth, ie number of yet unmatched {'s

banner = "/* Generated by haxecv */"

def applyRewriters(leavealone, l):
	""" apply the rewriters to string l, except the ones changing the strings
		in set "leavealone".
	"""
	global rewriters
	for re, cl in rewriters:
		if not cl in leavealone:
			l = re.sub(l, lambda m: m.bef + cl + m.aft)
	return l

def parseFile(filename):
	""" parse one .as file and generate the lines of the corresponding .hx file
	"""
	global comments, fors, depth
	
	leavealone = Set()			# strings that should be untouched by rewriters	
	
	withGetter = Set()			# list of names of members that have a getter
	withSetter = Set()			# list of names of members that have a setter
	className = ""				# name of the main class in the file
	hasACstr = False			# false iff the constructor is implicit
	inits = []					# (non-static member name, initial value) pairs
	fors = []					# fors is declared and described at the toplevel
	depth = 0					# ditto 
	hassuper = False			# true iff the constructor explicitly calls super()
	extends = ""				# name of the superclass
	needsComponentUI = 0		# specific to ASWing, <>0 iff ComponentUI should be
								# added to the imports
	
	yield banner + "\n"
	
	# *********  FIRST PASS
	for l in getLI(filename):
		
		#ignore comments
		if re_commentLine.search(l): continue

		#update depth
		depth += l.count('{') - l.count('}')
	
		#find the package name, import the qundash runtime library
		m = re_class.search(l)
		if m:
			className = m.name
			extends = m.ext
			yield "package " + m.prefix + ";\n\nimport qundash.Qundash;\n"
			continue
		
		#compute hassuper
		if re_super.search(l): hassuper = True
		
		#gather info about function declaration
		m = re_fun_decl.search(l)
		if m:
			if   m.name == className: hasACstr = True
			elif m.name == "setUI": needsComponentUI = 1
			if   m.setter == "get": withGetter.add(m.name)
			elif m.setter == "set": withSetter.add(m.name)

		#compute "inits"
		m = re_initializer.search(l)
		if depth == 1 and m and m.initvar and m.tags.find("static") < 0:
			inits.append((m.name, m.initvar))

			
	# *********  SECOND PASS
	depth = 0
	switchDepth = -1
	fors = []
	lines = open(filename).readlines()
	leadingws = ""

	while (lines != []):

		l = lines.pop(0)
	
		#remove <cr>'s
		l = l.replace("\r", "")
	
		#copy comments/white lines
		if l.strip() == "" or re_commentLine.search(l):
			yield l
			continue
		
		######imports
		#most code is specific to AsWing translation, but it still works fine for other files
		m = re_import.search(l)
		if m:
			if needsComponentUI == 1:	
				needsComponentUI = 2
				yield "import org.aswing.plaf.ComponentUI;\n"
			if m.name != "HashMap" and (needsComponentUI == 0 or m.name != "ComponentUI") :
				leavealone.add(m.name)
				yield l
			continue

		comments = ""

		#update depth
		depth += l.count('{') - l.count('}')
		if depth < switchDepth: switchDepth = -1

		#add increments for "for"s that have just been closed
		#does not work right if nested fors are closed on the same line
		pre = ""
		while (fors != [] and depth < fors[-1][0]):
			if l.count('}') > 1:
				comments += " !!! loop iterations may be misplaced"
			pre += leadingws + fors[-1][1] + ";\n"
			fors = fors[:-1]
		l = pre + l
			
		leadingws = re_startingws.search(l)._str
		
		######class declarations
		m = re_class.search(l)
		if m:
			#handle multiline class declarations properly
			l = applyRewriters(leavealone, l)
			while l.find('{') < 0:
				l += lines.pop(0)
				m = re_class.search(l)
			
			#build the haXe class declaration
			empty = True
			res = m.kind + " " + m.name + " "
			if m.ext:
				res += m.ext
				empty = False
			
			if m.impl:
				for inter in m.impl.split(", "):
					if not empty: res += ", "
					else: empty = False
					res += "implements " + inter
			
			res += m.rest + "\n"
			
			#generate a constructor if the .as file does not provide one
			if m.kind == 'class' and not hasACstr:
				ls = ["\n", "    public function " + className + "() {\n"]
				#initialize non-static members
				for var, value in inits:
					ls.append("        this." + var + value + ";\n")
				ls += ["    }\n"]
				lines = ls + lines
				     
			
			yield res
			continue
		

		######function declaration
		m = re_fun_decl.search(l)
		if m:
			#merge multiline function declarations into a single line
			while not m.rest:
				l += lines.pop(0)
				m = re_fun_decl.search(l)
				
			#compute the return type
			if m.type: ty = m.type
			else: ty = ":Dynamic"
			if m.name == "setUI": m.args = "(newUI:ComponentUI)"	# AsWing specific
			
			#make all args optional, set the missing arg types to Dynamic
			args = re_funarg.sub(m.args, lambda n:
				"?" + n.name + iff(n.type, n.type, ":Dynamic"))
			
			if m.setter:
				#if the function is a setter or getter
				l = ""
				if m.setter == "get" or not (m.name in withGetter):
					getter, setter = "null", "null"
					if m.name in withGetter: getter = "_get_" + m.name
					if m.name in withSetter: setter = "_set_" + m.name
					l = m.pre + "var " + m.name + "(" + getter + ", " + setter + ")" + ty + ";\n"
				l += m.pre + "function _" + m.setter + "_" + m.name + args + ty + "{\n"
			else:
				if m.name == className:
					#if the function is the constructor
					name = "new"
					ty = ""
					if extends and not hassuper:
						lines = ["        super();\n"] + lines
					if inits != []:
						lines = ["        this." + var + " " + value + ";\n"
								 for (var, value) in inits] + lines
				else: name = m.name
				l = m.pre + "function " + name + args + ty + m.after + "\n"
			l = applyRewriters(leavealone, l)
			yield l
			continue

		######member declaration
		if depth == 1:
			l = re_initializer.sub(l, lambda m:
				m.tags + "var " + m.name + iff(m.type, m.type, ":Dynamic")
					+ iff(m.tags.find("static") >= 0 and m.initvar, m.initvar, "") + ";")
		
		######converting "apply"
		l = re_apply.sub(l, lambda m:
			"Reflect.callMethod(" + sp(m.obj) + ", " + sp(m.fun))

		######converting splices that are used for insertion
		l = re_splice_insert.sub(l, lambda m:
			".insert(" + sp(m.index) + ", " + sp(m.value.strip()) + ")")
		
		######dictionary assignments (ie array[field] = value)
		# note that there's no way to tell dictionary assignment from an array assignment
		# when the value between [] is an id. We use a heuristic based on the id.
		startPos = -666
		def dictass(m):
			startPos = m._st
			if m.index: res = m._str
			else:
				res = "Reflect.setField(" + sp(m.name) + ", " + sp(m.selector) + ", " + sp(m.value) + ");"
				if m.field:
					global comments
					comments += " !!! could be " + m._str
			return res
		l = re_dict_aff.sub(l, dictass)		
		
		######converting for...in's
		l = re_for_in.sub(l, lambda m:
			"for (" + m.id + " in Reflect.fields(" + sp(m.obj) + "))")
		
		######converting for's with an incremented integer counter
		def forincr(m):
			if m.test == "<": sup = sp(m.max)
			else: sup = "(" + m.max + " + 1)"
			return "for (" + m.cnt + " in " + m.start + "..." + sup + ")"
		l = re_for_incr.sub(l, forincr)
		
		######converting other for's
		#m.rest is used to make sure that the line ends with a {; we don't want to
		#capture for's followed by a single command as they'd have to be handled differently
		def forother(m):
			global fors, depth
			fors.append((depth, m.next.strip()))
			return m.init + ";\n" + leadingws + "while(" + sp(m.test) + ")" + m.rest
		l = re_for_other.sub(l, forother)
		
		######converting delete
		l, delCnt = re_dict_delete.subn(l, lambda m:
			"Reflect.deleteField(" + m.name + ", " + m.selector + ")")
		
		######memorizing we're in a switch
		if re_switch.search(l):
			switchDepth = depth
		
		######removing switch breaks
		if switchDepth == depth: l = re_break.sub(l, "")
		
		######converting dictionary entry existence test
		def dictex(m):
			if m.index: return m._str
			else: return "Reflect.hasField(" + sp(m.name) + ", " + sp(m.selector) + ")"
		l = re_dict_exists.sub(l, dictex)
		
		######converting dictionary access (ie array[field])
		def dictacc(m):
			if m.index or startPos == m._st: return m._str
			res = "Reflect.field(" + sp(m.name) + ", " + sp(m.selector) + ")"
			if m.field:
				global comments
				comments += " !!! could be " + m._str
			return res
		if delCnt == 0: l = re_dict_access.sub(l, dictacc)
			 
		######converting typeof when used for equality testing
		#we convert it to an ActionScript instanceof, and then it will be
		#converted later to a Std.is or whatever function is appropriate
		l = re_typeof_cmp.sub(l, lambda m:
			"(" + sp(m.value) + ") instanceof " + m.cl.capitalize())

		######converting instanceof
		def instof(m):
			cl = m.cl.strip()
			v = sp(m.value.strip())
			if cl == "Function": return "Reflect.isFunction(" + v + ")"
			elif cl == "Object": return "Reflect.isObject(" + v + ")"
			else: return "Std.is(" + v + ", " + m.cl + ")"
		l = re_instanceof.recsub(l, instof)
		
		######converting ?: to if-else
		l = re_interro.recsub(l, lambda m:
			m.ret + "if (" + sp(m.test) + ") " + m.ifyes + " else " + m.ifno)
			
		######converting casts
		#note that a failed AS cast returns null, while a haXe cast() throws an exception.
		#thus the conversion is not straightforward
		def castfun(m):
			if m.isnew: return m._str
			elif m.classname == "String": return "Std.string(" + sp(m.value) + ")"
			elif m.classname == "Function":
				return "(if (Reflect.isFunction(" + sp(m.value) + ")) cast (" + sp(m.value) + ") else null)"
			elif m.classname == "Number":
				global comments
				comments += " !!! could be parseFloat"
				return "Std.parseInt(" + sp(m.value) + ")"
			else: return "(if (Std.is(" + sp(m.value) + ", " + m.classname + ")) cast(" + sp(m.value) + ", " + m.classname + ") else null)"
		l = re_cast.sub(l, castfun)

		l = applyRewriters(leavealone, l)
		if comments != "":
			l = leadingws + "/* QUNDASH" + comments + "*/\n" + l
		if l.strip() != "": yield l

					
def handleOneFile(filename):
	""" Convert xxxx.as to xxxx.hx . If the latter already exists, it is overwritten
		only if it starts with the qundash banner (see above).
	"""
	if filename[-3:] == ".as":
		hxname = filename[:-3] + ".hx"
		hxfile = None
		try:
			hxfile = open(hxname, 'r')
			pos = hxfile.readlines()[0].find(banner)
			hxfile.close()
			if pos < 0:
				print "Skipping", filename
				return
		except:
			if hxfile: hxfile.close()
		print "Converting", filename + "...",
		out = open(hxname, 'w')
		for l in parseFile(filename):
			out.write(l)
		out.close()
		print "done !"

if len(sys.argv) < 2:
	print "Usage:", sys.argv[0], " file1 [file2 file3]"
	print "       each argument that is a directory name is parsed recursively"
	print "       and all the .as files it contains are converted"

for arg in sys.argv[1:]:
	if arg[-3:] == ".as":
		handleOneFile(arg)
	else:
		for root, _, filenames in os.walk(arg):
			for filename in filenames:
				handleOneFile(os.path.join(root, filename))
	
